import 'dart:async';
import 'dart:math';

import 'package:flame/components.dart' hide Timer;
import 'package:flame/game.dart';
import 'package:flame/particles.dart';
import 'package:flame/sprite.dart';
import 'package:flame/timer.dart' as flame_timer;
import 'package:flame_examples/stories/utils/rotation_particle.dart';
import 'package:flutter/material.dart';

class ParticlesGame extends BaseGame {
  static const gridSize = 5.0;
  static const steps = 5;

  final Random rnd = Random();
  final StepTween steppedTween = StepTween(begin: 0, end: 5);
  final trafficLight = TrafficLightComponent();
  final TextPaint fpsTextPaint = TextPaint(
    config: const TextPaintConfig(
      color: Color(0xFFFFFFFF),
    ),
  );

  final sceneDuration = const Duration(seconds: 1);

  Vector2 get cellSize => size / gridSize;

  Vector2 get halfCellSize => cellSize / 2;

  @override
  Future<void> onLoad() async {
    await images.load('zap.png');
    await images.load('boom.png');

    Timer.periodic(sceneDuration, (_) => spawnParticles());
  }

  void spawnParticles() {
    final particles = <Particle>[
      circle(),
      smallWhiteCircle(),
      movingParticle(),
      randomMovingParticle(),
      alignedMovingParticles(),
      easedMovingParticle(),
      intervalMovingParticle(),
      computedParticle(),
      chainingBehaviors(),
      steppedComputedParticle(),
      reuseParticle(),
      imageParticle(),
      reuseImageParticle(),
      rotationImage(),
      acceleratedParticles(),
      paintParticle(),
      spriteParticle(),
      animationParticle(),
      fireworkParticle(),
      componentParticle(),
    ];

    do {
      final particle = particles.removeLast();
      final col = particles.length % gridSize;
      final row = (particles.length ~/ gridSize).toDouble();
      final cellCenter = (cellSize..multiply(Vector2(col, row))) + halfCellSize;
      add(
        TranslatedParticle(
          child: particle,
          offset: cellCenter.toOffset(),
          lifespan: 1,
        ).asComponent(),
      );
    } while (particles.isNotEmpty);
  }

  Particle circle() {
    return CircleParticle(paint: Paint()..color = Colors.white10);
  }

  Particle smallWhiteCircle() {
    return CircleParticle(
      paint: Paint()..color = Colors.white,
      radius: 5.0,
    );
  }

  Particle movingParticle() {
    return MovingParticle(
      child: CircleParticle(paint: Paint()..color = Colors.amber),
      from: Vector2(-20, -20).toOffset(),
      to: Vector2(20, 20).toOffset(),
    );
  }

  Particle randomMovingParticle() {
    return MovingParticle(
      child: CircleParticle(
        radius: 5 + rnd.nextDouble() * 5,
        paint: Paint()..color = Colors.red,
      ),
      to: randomCellVector2().toOffset(),
    );
  }

  Particle alignedMovingParticles() {
    return Particle.generate(
      count: 5,
      generator: (i) {
        final currentColumn = (cellSize.x / 5) * i - halfCellSize.x;
        return MovingParticle(
          child: CircleParticle(
            radius: 2.0,
            paint: Paint()..color = Colors.blue,
          ),
          from: Vector2(currentColumn, -halfCellSize.y).toOffset(),
          to: Vector2(currentColumn, halfCellSize.y).toOffset(),
        );
      },
    );
  }

  Particle randomMovingParticles() {
    return Particle.generate(
      count: 5,
      generator: (i) => MovingParticle(
        to: (randomCellVector2()..scale(.5)).toOffset(),
        child: CircleParticle(
          radius: 5 + rnd.nextDouble() * 5,
          paint: Paint()..color = Colors.deepOrange,
        ),
      ),
    );
  }

  Particle easedMovingParticle() {
    return Particle.generate(
      count: 5,
      generator: (i) => MovingParticle(
        curve: Curves.easeOutQuad,
        to: (randomCellVector2()..scale(.5)).toOffset(),
        child: CircleParticle(
          radius: 5 + rnd.nextDouble() * 5,
          paint: Paint()..color = Colors.deepPurple,
        ),
      ),
    );
  }

  Particle intervalMovingParticle() {
    return Particle.generate(
      count: 5,
      generator: (i) => MovingParticle(
        curve: const Interval(.2, .6, curve: Curves.easeInOutCubic),
        to: (randomCellVector2()..scale(0.5)).toOffset(),
        child: CircleParticle(
          radius: 5 + rnd.nextDouble() * 5,
          paint: Paint()..color = Colors.greenAccent,
        ),
      ),
    );
  }

  Particle computedParticle() {
    return ComputedParticle(
      renderer: (canvas, particle) => canvas.drawCircle(
        Offset.zero,
        particle.progress * halfCellSize.x,
        Paint()
          ..color = Color.lerp(
            Colors.red,
            Colors.blue,
            particle.progress,
          )!,
      ),
    );
  }

  Particle steppedComputedParticle() {
    return ComputedParticle(
      lifespan: 2,
      renderer: (canvas, particle) {
        const steps = 5;
        final steppedProgress = steppedTween.transform(particle.progress) / steps;
        canvas.drawCircle(
          Offset.zero,
          (1 - steppedProgress) * halfCellSize.x,
          Paint()
            ..color = Color.lerp(
              Colors.red,
              Colors.blue,
              steppedProgress,
            )!,
        );
      },
    );
  }

  Particle? reusableParticle;

  Particle reuseParticle() {
    reusableParticle ??= circle();

    return Particle.generate(
      generator: (i) => MovingParticle(
        curve: Interval(rnd.nextDouble() * .1, rnd.nextDouble() * .8 + .1),
        to: (randomCellVector2()..scale(.5)).toOffset(),
        child: reusableParticle!,
      ),
    );
  }

  Particle imageParticle() {
    return ImageParticle(
      size: Vector2.all(24),
      image: images.fromCache('zap.png'),
    );
  }

  Particle? reusableImageParticle;

  Particle reuseImageParticle() {
    const count = 9;
    const perLine = 3;
    const imageSize = 24.0;
    final colWidth = cellSize.x / perLine;
    final rowHeight = cellSize.y / perLine;

    reusableImageParticle ??= imageParticle();

    return Particle.generate(
      count: count,
      generator: (i) => TranslatedParticle(
        offset: Vector2(
          (i % perLine) * colWidth - halfCellSize.x + imageSize,
          (i ~/ perLine) * rowHeight - halfCellSize.y + imageSize,
        ).toOffset(),
        child: reusableImageParticle!,
      ),
    );
  }

  Particle rotationImage({double initialAngle = 0}) {
    return RotationParticle(from: initialAngle, child: imageParticle());
  }

  Particle acceleratedParticles() {
    return Particle.generate(
      generator: (i) => AcceleratedParticle(
        speed: (Vector2(rnd.nextDouble() * 600 - 300, -rnd.nextDouble() * 600) * .2).toOffset(),
        acceleration: Vector2(0, 200).toOffset(),
        child: rotationImage(initialAngle: rnd.nextDouble() * pi),
      ),
    );
  }

  Particle paintParticle() {
    final colors = [
      const Color(0xFFFF0000),
      const Color(0xFF00FF00),
      const Color(0xFF0000FF),
    ];
    final positions = [
      Vector2(-10, 10),
      Vector2(10, 10),
      Vector2(0, -14),
    ];

    return Particle.generate(
      count: 3,
      generator: (i) => PaintParticle(
        paint: Paint()..blendMode = BlendMode.difference,
        child: MovingParticle(
          curve: SineCurve(),
          from: positions[i].toOffset(),
          to: (i == 0 ? positions.last : positions[i - 1]).toOffset(),
          child: CircleParticle(
            radius: 20.0,
            paint: Paint()..color = colors[i],
          ),
        ),
      ),
    );
  }

  Particle spriteParticle() {
    return SpriteParticle(
      sprite: Sprite(images.fromCache('zap.png')),
      size: cellSize * .5,
    );
  }

  Particle animationParticle() {
    return SpriteAnimationParticle(
      animation: getBoomAnimation(),
      size: Vector2(128, 128),
    );
  }

  Particle componentParticle() {
    return MovingParticle(
      from: (-halfCellSize * .2).toOffset(),
      to: (halfCellSize * .2).toOffset(),
      curve: SineCurve(),
      child: ComponentParticle(component: trafficLight),
    );
  }

  Particle fireworkParticle() {
    final paints = [
      Colors.amber,
      Colors.amberAccent,
      Colors.red,
      Colors.redAccent,
      Colors.yellow,
      Colors.yellowAccent,
      Colors.blue,
    ].map((color) => Paint()..color = color).toList();

    return Particle.generate(generator: (i) {
      final initialSpeed = randomCellVector2();
      final deceleration = initialSpeed * -1;
      final gravity = Vector2(0, 40);

      return AcceleratedParticle(
        speed: initialSpeed.toOffset(),
        acceleration: (deceleration + gravity).toOffset(),
        child: ComputedParticle(
          renderer: (canvas, particle) {
            final paint = randomElement(paints);
            paint.color = paint.color.withOpacity(1 - particle.progress);

            canvas.drawCircle(
              Offset.zero,
              rnd.nextDouble() * particle.progress > .6
                  ? rnd.nextDouble() * (50 * particle.progress)
                  : 2 + (3 * particle.progress),
              paint,
            );
          },
        ),
      );
    });
  }

  Particle chainingBehaviors() {
    final paint = Paint()..color = randomMaterialColor();
    final rect = ComputedParticle(
      renderer: (canvas, _) => canvas.drawRect(
        Rect.fromCenter(center: Offset.zero, width: 10, height: 10),
        paint,
      ),
    );

    return ComposedParticle(
      children: [
        rect
            .rotating(to: pi / 2)
            .moving(to: -cellSize.toOffset())
            .scaled(2)
            .accelerated(acceleration: (halfCellSize * 5).toOffset())
            .translated(halfCellSize.toOffset()),
        rect
            .rotating(to: -pi)
            .moving(to: (Vector2(1, -1)..multiply(cellSize)).toOffset())
            .scaled(2)
            .translated((Vector2(1, -1)..multiply(halfCellSize)).toOffset())
            .accelerated(acceleration: (Vector2(-5, 5)..multiply(halfCellSize)).toOffset()),
      ],
    );
  }

  @override
  bool debugMode = true;

  @override
  void render(Canvas canvas) {
    super.render(canvas);

    if (debugMode) {
      fpsTextPaint.render(
        canvas,
        '${fps(120).toStringAsFixed(2)}fps',
        Vector2(0, size.y - 24),
      );
    }
  }

  Vector2 randomCellVector2() {
    return (Vector2.random() - Vector2.random())..multiply(cellSize);
  }

  Color randomMaterialColor() {
    return Colors.primaries[rnd.nextInt(Colors.primaries.length)];
  }

  T randomElement<T>(List<T> list) {
    return list[rnd.nextInt(list.length)];
  }

  SpriteAnimation getBoomAnimation() {
    const columns = 8;
    const rows = 8;
    const frames = columns * rows;
    final spriteImage = images.fromCache('boom.png');
    final spritesheet = SpriteSheet.fromColumnsAndRows(
      image: spriteImage,
      columns: columns,
      rows: rows,
    );
    final sprites = List<Sprite>.generate(frames, spritesheet.getSpriteById);
    return SpriteAnimation.spriteList(sprites, stepTime: 0.1);
  }
}

Future<BaseGame> loadGame() async {
  WidgetsFlutterBinding.ensureInitialized();
  return ParticlesGame();
}

class SineCurve extends Curve {
  @override
  double transformInternal(double t) {
    return (sin(pi * (t * 2 - 1 / 2)) + 1) / 2;
  }
}

class TrafficLightComponent extends Component {
  final Rect rect = Rect.fromCenter(center: Offset.zero, width: 32, height: 32);
  final flame_timer.Timer colorChangeTimer = flame_timer.Timer(2, repeat: true);
  final colors = [
    Colors.green,
    Colors.orange,
    Colors.red,
  ];

  TrafficLightComponent() {
    colorChangeTimer.start();
  }

  @override
  void render(Canvas c) {
    c.drawRect(rect, Paint()..color = currentColor);
  }

  @override
  void update(double dt) {
    super.update(dt);
    colorChangeTimer.update(dt);
  }

  Color get currentColor {
    return colors[(colorChangeTimer.progress * colors.length).toInt()];
  }
}
